2023-11-22

不看不知道,一看吓一跳,hooks真的需要好好掌握。我刚看了性能优化相关的hooks,发现真的是不用不行啊,如果不用的话,性能方面的问题我还真没有思路来解决。我看过腾讯出品的“扣钉”、用过钉钉开放平台网站,通过浏览器的插件知道它是用react写的,页面更新的时候很卡,说明腾讯、阿里对于react项目的优化也是有进步空间的。

好好学习react hooks,又没有收我的钱,还把笔记给我了,这样的好事真的是要牢牢抓住。可以这样学习:

1、先打开笔记,跟着老师的视频看一遍,千万不要跟着做,因为跟着做非常慢,很容易放弃。

2、看完一遍之后,回忆一下。再看第二遍,这一遍可以跟着做,因为代码有些熟悉了,可以很快写出来,跟着做的同时听老师的讲解。

3、自己看着笔记做一遍,项目应该会创建了,只需要得到效果即可。

4、后面就是不断复习了。

无论如何,明年一定要用上react。

1. 准备工作

  1. 基于 Vite 创建 React + TypeScript 的项目,具体创建项目的步骤,请参考 Vite 官方文档。

创建其实很简单,只是配置很难,参考:https://cn.vitejs.dev/guide/。预设的模板有这么多,直接输入命令行即可。

image-20231127085518506

直接创建:

其中react-test是项目名称,react-ts是模板名称。

也可以通过命令行选择一步一步的来操作(只需要输入npm create vite@latest,后面按提示操作即可)。(注意:不知道是怎么回事,在git bash里面这样操作是不行的,按箭头键是不起作用的。只能在cmd或powershell里面操作。)

  1. 在 Vite 项目中配置 @ 路径提示

    1. 安装 node 的类型声明:

    为什么需要配置@路径提示?

    因为这样很方便,在引入外部文件的时候,一般使用的是相对路径,但是相对路径不是很明确,接手的人不容易找到文件所在位置。

    如果是按照src目录下来找,就会很明确,依次查找文件即可。

    image-20231127092012401

    项目创建后,默认使用的就是相对路径。

    1. 配置 vite.config.ts 文件:

    image-20231127092236783

    只需要添加代码即可,原有的代码不需要动。

    1. 配置 tsconfig.json 文件,在 compilerOptions 节点下,新增 "baseUrl": ".""paths": { "@/*": [ "src/*" ] } 两项:

    这样,就可以使用@来表示/src/目录了。

    image-20231127092818057

    启动项目,没有问题。

2. useState

老师是这样用的,将App.tsx里面的组件代码清空,然后创建components文件夹,里面创建不同的文件夹,然后在里面创建不同的组件,通过在App.tsx里面引入这些组件,来讲解不同的hooks用法。这样就非常方便了,不用学习一个hooks就创建一个项目。

image-20231127094508687

image-20231127094552309

image-20231127094608991

在index.css里面更改样式:

image-20231127094851741

启动项目,运行OK:

image-20231127094957765

另外需要注意一点,react+ts项目,编写的组件文件必须是.tsx后缀,不能是.ts后缀,如果是.ts后缀,会报红色波浪线。

image-20231127095344415

改为.tsx后缀,就没有问题了。

image-20231127095417566

这应该可以在tsconfig.json里面进行配置,但是目前我还不知道该怎么配置,先保证不报错即可。

-----2024.02.26

应该不能进行配置,因为tsx文件里面就是编写组件的,ts文件无法识别组件的代码,所以会报红色波浪线。

react+ts项目中,vscode如何配置tab键生成标签?

在html中或者vue文件中,都可以使用tab键来生成标签,而且不需要什么配置,但是之前我配置过在react+js项目中使用tab生成标签,现在遇到react+ts项目了,tab键又不起作用了,怎么办?

打开vscode的settings,输入includeLanguages搜索,在Emmet这里面,添加规则:

image-20231127102424477

我在Indent-Rainbow里面也配置了,在ts文件也有了效果,但对tsx文件没有效果,还不知道怎么配置。

image-20231127102728966

在main.tsx里面,使用了<React.StrictMode>标签,这个标签的作用是什么呢?

对于第二点很好理解,但是第一点是什么意思?因为react全面推进函数式组件,所以“React offers a “Strict Mode” in which it calls each component’s function twice during development.”在开发者模式中,会执行两次组件函数,用来监测组件函数是不是“纯函数”。

那么在react开发过程中,可以把<React.StrictMode>模式去掉,等到打包时再加上。这样输出时就会显示得好一些。

基本用法

useState,能让函数组件拥有自己的状态,因此,它是一个管理状态的 hooks API。通过 useState 可以实现状态的初始化、读取、更新。基本语法格式如下:

其中:状态名所代表的数据,可以被函数组件使用;如果要修改状态名所代表的数据,需要调用 set 函数 进行修改。例如:

注意:

其实刚开始看到setCount(count + 1)这种用法的时候,真的不知道是什么意思、该怎么理解、很不习惯,怎么setCount和count一起用了?不一起用不行吗?因为这和普通调用函数的写法非常不一致,普通函数的调用方法是:传入参数即可,操作部分都不需要关心。

但是这个函数,似乎应该这样理解:让(count + 1)。这样理解也不对。

首先要理解count是什么?count就是定义的一个状态值,如果setCount需要用到原来的值,就要使用count,至于count+1,就是一个值而已,然后setCount拿到这个值,为count来赋值。

整个流程应该是这样的,把count+1看成一个结果,不要看成一个过程。

状态变化时,会触发函数组件的重新执行

在函数组件中使用 setState 定义状态之后,每当状态发生变化,都会触发函数组件的重新执行,从而根据最新的数据更新渲染 DOM 结构。例如:

注意:当函数式组件被重新执行时,不会重复调用 useState() 给数据赋初值,而是会复用上次的 state 值。

以函数的形式为状态赋初始值

在使用 useState 定义状态时,除了可以直接给定初始值,还可以通过函数返回值的形式,为状态赋初始值,语法格式如下:

例如:

注意:以函数的形式为状态赋初始值时,只有组件首次被渲染才会执行 fn 函数;当组件被更新时,会以更新前的值作为状态的初始值,赋初始值的函数不会执行。

useState 是异步变更状态的

调用 useState() 会返回一个变更状态的函数,这个函数内部是以异步的形式修改状态的,所以修改状态后无法立即拿到最新的状态,例如:

在上述代码的第8行,打印出来的 count 值是更新前的旧值,而非更新后的新值。证明 useState 是异步变更状态的。

结合 useEffect 监听状态的变化

为了能够监听到状态的变化,react 提供了 useEffect 函数。它能够监听依赖项状态的变化,并执行对应的回调函数。基本语法格式如下:

例如:

image-20231127111331001

注意:useEffect 也是 React 提供的 Hooks API,后面的课程中会对它进行详细的介绍。

注意事项

1. 更新对象类型的值

如果要更新对象类型的值,并触发组件的重新渲染,则必须使用展开运算符Object.assign()生成一个新对象,用新对象覆盖旧对象,才能正常触发组件的重新渲染。示例代码如下:

2. 解决值更新不及时的 Bug

当连续多次以相同的操作更新状态值时,React 内部会对传递过来的新值进行比较,如果值相同,则会屏蔽后续的更新行为,从而防止组件频繁渲染的问题。这虽然提高了性能,但也带来了一个使用误区,例如:

经过测试,我们发现上述代码执行的结果,只是让 count 从 0 变成了 1,最终的 count 值并不是 2。Why?

因为 setCount 是异步地更新状态值的,那么前后两次调用 setCount 的代码执行是同步的,传递到异步更新队列里面的新值都是 1。React 内部如果遇到两次相同的状态,则会默认阻止组件再次更新。

为了解决上述的问题,我们可以使用函数的方式给状态赋新值。当函数执行时才通过函数的形参,拿到当前的状态值,并基于它返回新的状态值。示例代码如下:

3. 使用 setState 模拟组件的强制刷新

在函数组件中,我们可以通过 useState 来模拟 forceUpdate 的强制刷新操作。因为只要 useState 的状态发生了变化,就会触发函数组件的重新渲染,从而达到强制刷新的目的。具体的代码示例如下:

注意:因为每次传入的对象的地址不同,所以一定会使组件刷新。

3. useRef

useRef 函数返回一个可变的 ref 对象,该对象只有一个 current 属性。可以在调用 useRef 函数时为其指定初始值。并且这个返回的 ref 对象在组件的整个生命周期内保持不变。语法格式如下:

useRef 函数用来解决以下两个问题:

  1. 获取 DOM 元素子组件的实例对象;
  2. 存储渲染周期之间共享的数据

获取 DOM 元素的实例

下面的代码演示了如何获取 Input 元素的实例,并调用其 DOM API。

上面在使用useRef的时候,定义了类型const iptRef = useRef<HTMLInputElement>(null),其实我对这些类型还是很害怕的,因为react的一些类型(不只是react,还有vue项目)我都不知道该到哪里去找,原来想好的方法也就是遇见一个记一个,但这很显然不能减少我的恐惧。

在这个项目中,有类型提示了,多多少少算是一种便利。

存储渲染周期之间的共享数据

基于 useRef 创建名为 prevCountRef 的数据对象,用来存储上一次的旧 count 值。每当点击按钮触发 count 自增时,都把最新的旧值赋值给 prevCountRef.current 即可:

注意事项

1. 组件 rerender 时 useRef 不会被重复初始化

RefTimer 组件中,点击 +1 按钮,会让 count 值自增,从而触发 RefTimer 组件的 rerender。但是,我们发现 RefTimer 组件中的时间戳保持不变,这说明组件每次渲染,不会重复调用 useRef 函数进行初始化。示例代码如下:

2. ref.current 变化时不会造成组件的 rerender

点击给 ref 赋新值的按钮时,为 time.current 赋新值,执行的结果是:

  1. 终端中输出了最新的 time.current 的值
  2. 没有触发 RefTimer 组件的 rerender

这证明了 ref.current 变化时不会造成组件的 rerender,示例代码如下:

react迷人之处

这个看上去很难理解,因为updateTime和console都在同一个函数中,为什么执行了updateTime,而它的下一行代码console就不会执行呢?

这完全超乎我的经验和想象。这就是JS和react的迷人之处了,不知道react是怎么处理的,代码粒度非常细,估计是将updateTime抽离出去,放到不同的流程来执行。

3. ref.current 不能作为其它 Hooks 的依赖项

由于 ref.current 值的变化不会造成组件的 rerender,而且 React 也不会跟踪 ref.current 的变化,因此 ref.current 不可以作为其它 hooks(useMemo、useCallback、useEffect 等) 的依赖项。

image-20231127143031897

在上面的代码中,组件首次渲染完成后,必然会触发一次 useEffect 的执行。但是,当 time.current 发生变化时,并不会触发 useEffect 的重新执行。因此,不能把 ref.current 作为其它 hooks 的依赖项。

当使用ref.current作为其它hooks的依赖项时,vscode会提示:

image-20231127142820255

4. forwardRef

ref 的作用是获取实例,但由于函数组件不存在实例,因此无法通过 ref 获取函数组件的实例引用。而 React.forwardRef 就是用来解决这个问题的。(之前学习useRef的时候,如果是指定为元素的ref属性,都是指定为已知的html元素上面的,但是不能指定到自定义的函数式组件上。)

React.forwardRef 会创建一个 React 组件,这个组件能够将其接收到的 ref 属性转发到自己的组件树。

注意React.forwardRef不是hooks,而是react的一个api。

无法直接使用 ref 引用函数式组件

在下面的例子中,父组件 Father 想通过 ref 引用子组件 Child,此时代码会报错,因为函数式组件没有实例对象,无法被直接引用:

Child 组件的定义如下:

注意:上面的代码可以运行,但会在终端提示如下的 Warning 警告:

image-20231128084958741

父组件中输出的内容为:{ current: null },说明是无法拿到Child组件的ref的。

错误提示中有解决此问题的关键提示:Did you mean to use React.forwardRef()?

forwardRef 的基本使用

在使用函数组件时,我们无法直接使用 ref 引用函数式组件,下面的代码会产生报错:

因为默认情况下,你自己的组件不会暴露它们内部 DOM 节点的 ref。

正确的方法是使用 React.forwardRef() 把函数式组件包裹起来(只需要包裹起来即可,该怎么定义函数式组件就怎么定义。),例如 Child 子组件的代码如下:

可以直接从react中导入forwardRef方法:

注意:当使用React.forwardRef定义组件之后,就不能将Child组件的类型定义为React.FC了,但是定义为什么类型,我看到老师的vscode里面好像有提示,但是我的vscode里面是没有提示的,所以暂时不管吧。

-----2024.02.26

image-20240226105536147

查看react官方的ts定义,forwardRef需要传递两个泛型参数进去,那T和P应该怎么写呢?继续查看ForwardRefRenderFunction里面的定义:

image-20240226105713505

可以看到,P就可以定义为自定义的对象类型,但是T还是不明确,继续查看ForwardedRef里面是怎么定义T的:

image-20240226105842146

还是不明确T的类型定义,继续查看MutableRefObject是怎么定义T的:

image-20240226105942523

从这个定义来看,似乎T可以随便定义,那这个案例这里怎么定义呢?

确实是可以随便定义的,是根据ref的类型来定义的,T决定了ref的类型,需要注意的是,函数中定义泛型和使用参数的顺序是反的,所以泛型第一个参数是T,即ref的类型,第二个泛型参数是P,可以省略:

其实我之前都做出来了,类型都写对了,但是<Child ref={childRef} />的ref下面一直有红色波浪线,搞得我找不到错在哪里,它的报错信息是这样的,我也看不懂:

image-20240226112339253

后来发现,useRef里面没有赋初始值,改成这样之后就没有报错了:useRef(null)。其实我应该看懂的,里面有一句:Type 'ChildRefType | undefined' is not assignable to type 'ChildRefType | null'. Type 'undefined' is not assignable to type 'ChildRefType | null'.ts(2322)

应该已经提示我了,undefined类型对不上null类型,应该显式的赋值为null。阮一峰的教程里面应该讲到了。

然后,在父组件 Father 中,就可以给子组件 Child 绑定 ref 了:

注意:此时父组件 Father 中获取到的 ref.current 是 null,因为子组件 Child 没有向外暴露任何自己内部的东西。

怎么向外暴露子组件里面的DOM呢?将ref传递到要公开的DOM结点中即可:

在Father组件中,获取实例:

效果:

5. useImperativeHandle

直接使用 ref 获取 DOM 实例,会全面暴露 DOM 实例上的 API,从而导致外部使用 ref 时有更大的自由度。在实际开发中,我们应该严格控制 ref 的暴露颗粒度,控制它能调用的方法,只向外暴露主要的功能函数,其它功能函数不暴露。

React 官方提供 useImperativeHandle 的目的,就是让你在使用 ref 时可以自定义暴露给外部组件哪些功能函数或属性。

它的语法结构如下:

其中,第三个参数(依赖项数组)是可选的。

第一个参数,就是使用React.forwardRef定义函数式组件时,会有两个参数,第一个参数是props,第二个参数是ref。那么这里的第一个参数就是ref,直接拿过来就行了。

第二个参数就是需要暴露出去的对象,写成了函数返回值的形式,注意一定要返回值。

useImperativeHandle 的基本使用

在被 React.forwardRef() 包裹的组件中,需要结合 useImperativeHandle 这个 hooks API,向外按需暴露子组件内的成员:

打印结果:

基于 useImperativeHandle 按需向外暴露成员

在子组件中,向外暴露 count 和 setCount 这两个成员:

在父组件中,添加一个重置按钮,当点击重置按钮时,调用 ref 向外暴露的 setCount 函数,把子组件内部的 count 重置为 0。示例代码如下:

控制成员暴露的粒度

在 Child 子组件中,我们希望对外暴露一个重置 count 为 0 的函数,而不希望直接把 setCount() 暴露出去,因为父组件调用 setCount() 时可以传任何数值,这样又要写校验的代码,很麻烦。因此,我们可以基于 useImperativeHandle,向外提供一个 reset() 函数而非直接把 setCount() 暴露出去:

在父组件中,调用 ref.current.reset() 即可把数据重置为 0:

useImperativeHandle 的第三个参数

再来回顾一下 useImperativeHandle 的参数项:

其中,第三个参数有3种用法:

  1. 空数组:只在子组件首次被渲染时,执行 useImperativeHandle 中的 fn 回调,从而把 return 的对象作为父组件接收到的 ref。例如:

    可以看到,外界拿到的count值,永远是初次渲染时的值。

  2. 依赖项数组:子组件首次被渲染时,或依赖项改变时,会执行 useImperativeHandle 中的 fn 回调,从而让父组件通过 ref 能拿到依赖项的新值。例如:

  3. 省略依赖项数组(省略第三个参数):此时,组件内任何 state 的变化,都会导致 useImperativeHandle 中的回调的重新执行。因此如果使用useImperativeHandle,那么第三个参数不能省略,否则会导致性能问题,除非是特殊目的。示例代码如下:

陷阱1:不要滥用 ref。 父子、祖孙的传值、传方法,首先就应该想到props,这是最简单的方法,也是react最推荐的方法。

你应当仅在你没法通过 prop 来表达 命令式 行为的时候才使用 ref:例如,滚动到指定节点、聚焦某个节点、触发一次动画,以及选择文本等等。

 

陷阱2:如果可以通过 prop 实现,那就不应该使用 ref。例如,你不应该从一个 Model 组件暴露出 {open, close} 这样的命令式句柄,最好是像 <Modal isOpen={isOpen} /> 这样,将 isOpen 作为一个 prop。副作用 可以帮你通过 prop 来暴露一些命令式的行为。